Petals to the Metal - Flower Classification on TPU
Getting Started with TPUs on Kaggle!
import numpy as np
import pandas as pd
import seaborn as sns
import albumentations as A
import matplotlib.pyplot as plt
import os, gc, cv2, random, warnings, math, sys, json, pprint, pdb
import tensorflow as tf
from tensorflow.keras import backend as K
import tensorflow_hub as hub
from sklearn.model_selection import train_test_split
!pip install kaggle
DEVICE = 'TPU' #@param ["None", "'GPU'", "'TPU'"] {type:"raw", allow-input: true}
if DEVICE == "TPU":
print("connecting to TPU...")
try:
tpu = tf.distribute.cluster_resolver.TPUClusterResolver()
print('Running on TPU ', tpu.master())
except ValueError:
print("Could not connect to TPU")
tpu = None
if tpu:
try:
print("initializing TPU ...")
tf.config.experimental_connect_to_cluster(tpu)
tf.tpu.experimental.initialize_tpu_system(tpu)
strategy = tf.distribute.experimental.TPUStrategy(tpu)
print("TPU initialized")
except _:
print("failed to initialize TPU")
else:
DEVICE = "GPU"
if DEVICE != "TPU":
print("Using default strategy for CPU and single GPU")
strategy = tf.distribute.get_strategy()
if DEVICE == "GPU":
print("Num GPUs Available: ", len(tf.config.experimental.list_physical_devices('GPU')))
AUTOTUNE = tf.data.experimental.AUTOTUNE
REPLICAS = strategy.num_replicas_in_sync
print(f'REPLICAS: {REPLICAS}')
def seed_everything(seed=0):
random.seed(seed)
np.random.seed(seed)
tf.random.set_seed(seed)
os.environ['PYTHONHASHSEED'] = str(seed)
os.environ['TF_DETERMINISTIC_OPS'] = '1'
def is_colab():
return 'google.colab' in str(get_ipython())
#@title Debugger { run: "auto" }
SEED = 16
DEBUG = True #@param {type:"boolean"}
TRAIN = True #@param {type:"boolean"}
INFERENCE = True #@param {type:"boolean"}
IS_COLAB = is_colab()
warnings.simplefilter('ignore')
seed_everything(SEED)
print(f"Using TensorFlow v{tf.__version__}")
if IS_COLAB:
from google.colab import drive
drive.mount('/content/gdrive', force_remount=True)
GCS_PATTERN = 'gs://flowers-public/*/*.jpg'
GCS_OUTPUT = 'gs://flowers-public/tfrecords-jpeg-192x192-2/flowers'
SHARDS = 16
TARGET_SIZE = [192, 192]
CLASSES = [b'daisy', b'dandelion', b'roses', b'sunflowers', b'tulips']
from google.colab import files
uploaded = files.upload()
for fn in uploaded.keys():
print('User uploaded file "{name}" with length {length} bytes'.format(
name=fn, length=len(uploaded[fn])))
# Then move kaggle.json into the folder where the API expects to find it.
!mkdir -p ~/.kaggle/ && mv kaggle.json ~/.kaggle/ && chmod 600 ~/.kaggle/kaggle.json
project_name = 'tpu-getting-started'
root_path = '/content/gdrive/MyDrive/' if IS_COLAB else '/'
input_path = f'{root_path}kaggle/input/{project_name}/'
working_path = f'{input_path}working/' if IS_COLAB else '/kaggle/working/'
os.makedirs(working_path, exist_ok=True)
os.chdir(input_path)
!kaggle competitions download -c tpu-getting-started
os.chdir(working_path)
os.listdir(input_path)
def decode_image_and_label(filename):
bits = tf.io.read_file(filename)
image = tf.image.decode_jpeg(bits)
label = tf.strings.split(tf.expand_dims(filename, axis=-1), sep='/')
#label = tf.strings.split(filename, sep='/')
label = label.values[-2]
return image, label
for x in filenames.take(10): print(x)
ds = filenames.map(decode_image_and_label, num_parallel_calls=AUTOTUNE)
def show_images(ds):
_,axs = plt.subplots(3,3,figsize=(16,16))
for ((x, y), ax) in zip(ds.take(9), axs.flatten()):
ax.imshow(x.numpy().astype(np.uint8))
ax.set_title(y.numpy().decode("utf-8"))
ax.axis('off')
show_images(ds)
Check how many images are available in the training dataset and also check if each item in the training set are unique
BASE_MODEL, IMG_SIZE = ('efficientnet_b3', 300) #@param ["('efficientnet_b3', 300)", "('efficientnet_b4', 380)", "('efficientnet_b2', 260)"] {type:"raw", allow-input: true}
BATCH_SIZE = 32 #@param {type:"integer"}
IMG_SIZE = (IMG_SIZE, IMG_SIZE) #@param ["(IMG_SIZE, IMG_SIZE)", "(512,512)"] {type:"raw"}
print("Using {} with input size {}".format(BASE_MODEL, IMG_SIZE))
Loading data
After my quick and rough EDA, let's load the PIL Image to a Numpy array, so we can move on to data augmentation.
In fastai, they have item_tfms and batch_tfms defined for their data loader API. The item transforms performs a fairly large crop to 224 and also apply other standard augmentations (in aug_tranforms) at the batch level on the GPU. The batch size is set to 32 here.
train_df, valid_df = train_test_split(
df
,test_size = 0.2
,random_state = SEED
,shuffle = True
,stratify = df['label'])
train_ds = tf.data.Dataset.from_tensor_slices(
(train_df.filename.values,train_df.label.values))
valid_ds = tf.data.Dataset.from_tensor_slices(
(valid_df.filename.values, valid_df.label.values))
adapt_ds = tf.data.Dataset.from_tensor_slices(
train_df.filename.values)
for x,y in valid_ds.take(3): print(x, y)
def decode_image(filename):
img = tf.io.read_file(filename)
img = tf.image.decode_jpeg(img, channels=3)
return img
def collate_train(filename, label):
img = decode_image(filename)
img = tf.image.random_brightness(img, 0.3)
img = tf.image.random_flip_left_right(img, seed=None)
img = tf.image.random_crop(img, size=[*IMG_SIZE, 3])
return img, label
def process_adapt(filename):
img = decode_image(filename)
img = tf.keras.layers.experimental.preprocessing.Rescaling(1.0 / 255)(img)
return img
def collate_valid(filename, label):
img = decode_image(filename)
img = tf.image.resize(img, [*IMG_SIZE])
return img, label
train_ds = train_ds.map(collate_train, num_parallel_calls=AUTOTUNE)
valid_ds = valid_ds.map(collate_valid, num_parallel_calls=AUTOTUNE)
adapt_ds = adapt_ds.map(process_adapt, num_parallel_calls=AUTOTUNE)
def show_images(ds):
_,axs = plt.subplots(4,6,figsize=(24,16))
for ((x, y), ax) in zip(ds.take(24), axs.flatten()):
ax.imshow(x.numpy().astype(np.uint8))
ax.set_title(np.argmax(y))
ax.axis('off')
show_images(train_ds)
show_images(valid_ds)
train_ds_batch = (train_ds
.cache('dump.tfcache')
.shuffle(buffer_size=1000)
.batch(BATCH_SIZE)
.prefetch(buffer_size=AUTOTUNE))
valid_ds_batch = (valid_ds
#.shuffle(buffer_size=1000)
.batch(BATCH_SIZE*2)
.prefetch(buffer_size=AUTOTUNE))
adapt_ds_batch = (adapt_ds
.shuffle(buffer_size=1000)
.batch(BATCH_SIZE)
.prefetch(buffer_size=AUTOTUNE))
data_augmentation = tf.keras.Sequential(
[
tf.keras.layers.experimental.preprocessing.RandomCrop(*IMG_SIZE),
tf.keras.layers.experimental.preprocessing.RandomFlip("horizontal_and_vertical"),
tf.keras.layers.experimental.preprocessing.RandomRotation(0.25),
tf.keras.layers.experimental.preprocessing.RandomZoom((-0.2, 0)),
tf.keras.layers.experimental.preprocessing.RandomContrast((0.2,0.2))
]
)
func = lambda x,y: (data_augmentation(x), y)
x = (train_ds
.batch(BATCH_SIZE)
.take(1)
.map(func, num_parallel_calls=AUTOTUNE))
show_images(x.unbatch())
from tensorflow.keras.applications import EfficientNetB3
efficientnet = EfficientNetB3(
weights = 'imagenet' if TRAIN else None,
include_top = False,
input_shape = (*IMG_SIZE, 3),
pooling='avg')
def build_model(base_model, num_class):
inputs = tf.keras.layers.Input(shape=(*IMG_SIZE, 3))
x = data_augmentation(inputs)
x = base_model(x)
x = tf.keras.layers.Dropout(0.4)(x)
outputs = tf.keras.layers.Dense(num_class, activation="softmax", name="pred")(x)
model = tf.keras.models.Model(inputs=inputs, outputs=outputs)
return model
model = build_model(base_model=efficientnet, num_class=len(id2label))
model.summary()
The 3rd layer of the Efficient is the Normalization layer, which can be tuned to our new dataset instead of imagenet. Be patient on this one, it does take a bit of time as we're going through the entire training set.
%%time
if TRAIN:
if not os.path.exists(f"{working_path}000_normalization.h5"):
model.get_layer('efficientnetb3').get_layer('normalization').adapt(adapt_ds_batch)
model.save_weights("000_normalization.h5")
else:
model.load_weights("000_normalization.h5")
CosineDecayRestarts function implemented in tf.keras as it seemed promising and I struggled to find the right settings (if there were any) for the ReduceLROnPlateau
EPOCHS = 8
STEPS = int(round(len(train_df)/BATCH_SIZE)) * EPOCHS
schedule = tf.keras.experimental.CosineDecayRestarts(
initial_learning_rate=1e-4,
first_decay_steps=300
)
schedule.get_config()
x = [i for i in range(STEPS)]
y = [schedule(s) for s in range(STEPS)]
plt.plot(x, y)
LearningRateScheduler that tensorflow gives us. The LearningRateScheduler update the lr on_epoch_begin while it makes more sense to do it on_batch_end or on_batch_begin.
callbacks = [
tf.keras.callbacks.ModelCheckpoint(
filepath='001_best_model.h5',
monitor='val_loss',
save_best_only=True),
]
model.compile(loss="sparse_categorical_crossentropy",
optimizer=tf.keras.optimizers.Adam(schedule),
metrics=["accuracy"])
if TRAIN:
history = model.fit(train_ds_batch,
epochs = EPOCHS,
validation_data=valid_ds_batch,
callbacks=callbacks)
def plot_hist(hist):
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.title('Loss over epochs')
plt.ylabel('loss')
plt.xlabel('epoch')
plt.legend(['train', 'valid'], loc='best')
plt.show()
if TRAIN:
plot_hist(history)
We load the best weight that were kept from the training phase. Just to check how our model is performing, we will attempt predictions over the validation set. This can help to highlight any classes that will be consistently miscategorised.
model.load_weights('001_best_model.h5')
x = train_df.sample(1).filename.values[0]
img = decode_image(x)
%%time
imgs = [tf.image.random_crop(img, size=[*IMG_SIZE, 3]) for _ in range(4)]
_,axs = plt.subplots(1,4,figsize=(16,4))
for (x, ax) in zip(imgs, axs.flatten()):
ax.imshow(x.numpy().astype(np.uint8))
ax.axis('off')
I apply some very basic test time augmentation to every local image extracted from the original 600-by-800 images. We know we can do some fancy augmentation with albumentations but I wanted to do that exclusively with Keras preprocessing layers to keep the cleanest pipeline possible.
tta = tf.keras.Sequential(
[
tf.keras.layers.experimental.preprocessing.RandomCrop((*IMG_SIZE)),
tf.keras.layers.experimental.preprocessing.RandomFlip("horizontal_and_vertical"),
tf.keras.layers.experimental.preprocessing.RandomZoom((-0.2, 0.2)),
tf.keras.layers.experimental.preprocessing.RandomContrast((0.2,0.2))
]
)
def predict_tta(filename, num_tta=4):
img = decode_image(filename)
img = tf.expand_dims(img, 0)
imgs = tf.concat([tta(img) for _ in range(num_tta)], 0)
preds = model.predict(imgs)
return preds.sum(0).argmax()
pred = predict_tta(df.sample(1).filename.values[0])
print(pred)
if INFERENCE:
from tqdm import tqdm
preds = []
with tqdm(total=len(valid_df)) as pbar:
for filename in valid_df.filename:
pbar.update()
preds.append(predict_tta(filename, num_tta=4))
if INFERENCE:
cm = tf.math.confusion_matrix(valid_df.label.values, np.array(preds))
plt.figure(figsize=(10, 8))
sns.heatmap(cm,
xticklabels=id2label.values(),
yticklabels=id2label.values(),
annot=True,
fmt='g',
cmap="Blues")
plt.xlabel('Prediction')
plt.ylabel('Label')
plt.show()
test_folder = input_path + '/test_images/'
submission_df = pd.DataFrame(columns={"image_id","label"})
submission_df["image_id"] = os.listdir(test_folder)
submission_df["label"] = 0
submission_df['label'] = (submission_df['image_id']
.map(lambda x : predict_tta(test_folder+x)))
submission_df
submission_df.to_csv("submission.csv", index=False)
1% Better Everyday
reference
- https://www.kaggle.com/ryanholbrook/tfrecords-basics
- https://www.kaggle.com/ryanholbrook/create-your-first-submission
- https://colab.research.google.com/github/GoogleCloudPlatform/training-data-analyst/blob/master/courses/fast-and-lean-data-science/03_Flower_pictures_to_TFRecords.ipynb#scrollTo=MPkvHdAYNt9J
- https://www.kaggle.com/c/flower-classification-with-tpus/discussion/130326
- https://codelabs.developers.google.com/codelabs/keras-flowers-data/#5
todos
done